#!/usr/bin/python2.5
#
# zbuilder -- automated backporter
# Basic usage: zbuilder [options] [arguments]
#
# Copyright (C) 2007 Zach Tibbitts <zach@collegegeek.org>
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#

import os, os.path, glob, shutil, platform, sys
from optparse import OptionParser

APT_URL = 'http://archive.ubuntu.com/ubuntu'
WORKING_DIR = '/var/cache/zbuilder'
SUFFIXES = {'warty':	'4.10zbuild1',
			'hoary':	'5.04zbuild1',
			'breezy':	'5.10zbuild1',
			'dapper':	'6.06zbuild1',
			'edgy':		'6.10zbuild1',
			'feisty':	'7.04zbuild1',
			'gutsy':	'7.10zbuild1'}
VALID_DIST = SUFFIXES.keys()
VALID_ARCH = ('i386', 'amd64', 'powerpc', 'sparc')
BIN_OP = {	'<' : 'lt',
			'<<': 'lt',
			'<=': 'le',
			'=' : 'eq',
			'>=': 'ge',
			'>>': 'gt',
			'>' : 'gt'}

class NoBuildEnvironment(Exception): pass
class DependencyError(Exception): pass

def create_build_env(distro, arch):
	name = distro + '-' + arch
	build_path = os.path.join(WORKING_DIR, 'env', name)
	os.makedirs(build_path)
	os.system('debootstrap --arch %s %s %s %s'%(arch, distro, build_path, APT_URL))
	f = open(os.path.join(build_path, 'etc', 'apt', 'sources.list'), 'w')
	f.write('deb http://archive.ubuntu.com/ubuntu/ %s main restricted universe multiverse\n'%distro)
	f.write('deb-src http://archive.ubuntu.com/ubuntu/ %s main restricted universe multiverse\n'%distro)
	f.write('deb http://archive.ubuntu.com/ubuntu/ %s-updates main restricted universe multiverse\n'%distro)
	f.write('deb-src http://archive.ubuntu.com/ubuntu/ %s-updates main restricted universe multiverse\n'%distro)
	f.write('deb http://security.ubuntu.com/ubuntu/ %s-security main restricted universe multiverse\n'%distro)
	f.write('deb-src http://security.ubuntu.com/ubuntu/ %s-security main restricted universe multiverse\n'%distro)
	f.write('deb http://archive.ubuntu.com/ubuntu/ %s-backports main restricted universe multiverse\n'%distro)
	f.write('deb-src http://archive.ubuntu.com/ubuntu/ %s-backports main restricted universe multiverse\n'%distro)
	f.flush()
	f.close()
	apt_list = []
	for x in os.popen('COLUMNS=132 dpkg -l | grep language-pack').readlines():
		apt_list.append(x.strip('\n').split()[1])
	os.system('chroot %s apt-get -y update'%build_path)
	os.system('chroot %s apt-get -y install %s'%(build_path, ' '.join(apt_list)))
	os.system('chroot %s dpkg-reconfigure locales'%build_path)
	os.system('chroot %s apt-get -y install build-essential'%build_path)
	os.makedirs(build_path + WORKING_DIR)
	os.chdir(os.path.join(WORKING_DIR, 'env'))
	os.system('tar -czf %s.zbuild.tgz %s/'%(name, name))
	os.system('rm -rf %s'%build_path)

def get_current_distro():
	return os.popen('lsb_release -c').read().split(':')[1].strip()

def get_current_arch():
	return os.popen('dpkg --print-architecture').read().strip()

class Backport:
	def __init__(self):
		raise NotImplementedError
	def store_values(self, distro, arch, put, suffix, force):
		self.target_distro = distro
		self.target_arch = arch
		self.distro_name = distro+'-'+arch
		self.dest_dir = put
		self.append_suffix = suffix
		self.force_dep = force
	def check_for_env(self):
		if not os.path.exists('/var/cache/zbuilder/env/%s.zbuild.tgz'%self.distro_name):
			raise NoBuildEnvironment(
				"Not ready to build for %s. You need to run 'zbuilder -d %s -a %s create'." % \
				(self.distro_name, self.target_distro, self.target_arch))
	def init_working_path(self):
		self.working_path=os.path.join(WORKING_DIR, 'src', str(os.getpid()))
		while os.path.exists(self.working_path):
			self.working_path+='.1'
		os.makedirs(self.working_path)
	def prepare_source(self):
		raise NotImplementedError
	def mount_build_env(self):
		os.chdir(os.path.join(WORKING_DIR, 'env'))
		os.system('tar -xzf %s.zbuild.tgz'%self.distro_name)
		os.system('mount --bind %s %s/env/%s%s'%(WORKING_DIR, WORKING_DIR, self.distro_name, WORKING_DIR))
		self.chroot_path = '%s/env/%s'%(WORKING_DIR, self.distro_name)
	def debian_version(self):
		os.chdir(self.source_dir)
		version=os.popen('cat debian/changelog | head -n 1 | cut -d \( -f 2 | cut -d \) -f 1').read()[:-1]
		if not version:
			raise ValueError("Could not determine source version")
		return version
	def build_version(self):
		if self.append_suffix:
			if self.debian_version().endswith('~'+SUFFIXES[self.target_distro]):
				return self.debian_version()
			else:
				return self.debian_version()+'~'+SUFFIXES[self.target_distro]
		else:
			return debian_version()
	def dch_version(self):
		os.chdir(self.source_dir)
		os.system('dch -v %s -b "Automated build by zbuilder"'%self.build_version())
	def check_depends(self):
		os.chdir(self.source_dir)
		f = open(os.path.join(self.source_dir, 'debian','control'), 'r')
		for line in f.readlines():
			if line.startswith('Build-Depends'):
				depends = line[15:].split(',')
		if depends:
			apt_list = []
			for dep in depends:
				dep = dep.strip()
				print >> sys.stderr, "I: Trying to resolve dependency:", dep
				if dep.count(' '): # item has a version number
					dep_pkg, dep_ver = dep.split(' ', 1)
				else:
					dep_pkg = dep
					dep_ver = None
				# Check to see if the package exists:
				try:
					for line in os.popen('chroot %s apt-cache madison %s'%(self.chroot_path, dep_pkg)).readlines():
						line = line.strip('\n')
						if line.endswith('Packages'):
							result, version, repo = line.split(' | ')
							break
						else:
							if not self.force_dep:
								raise DependencyError("E: Couldn't satisfy dependency %s, use -f to override"%dep_pkg)
					print >> sys.stderr, "    Trying:", result, version,
					if dep_ver is not None:
						# Need to check version numbers of dependencies
						op, vn = dep_ver.strip('()').split(' ', 1)
						op = BIN_OP[op]
						check = os.system('dpkg --compare-versions %s %s %s'%(version, op, vn))
						if check is 0:
							print >> sys.stderr, "OK"
							apt_list.append(dep_pkg)
						elif self.force_dep:
							print >> sys.stderr, "\nW: %s %s does not satisfy %s"%(result, version, dep)
							print >> sys.stderr, "W: Building anyways because -f was used"
							apt_list.append(dep_pkg)
						else:
							raise DependencyError("\nE: Couldn't satisfy dependency %s, use -f to override"%dep_pkg)
					else:
						apt_list.append(dep_pkg)
				except ValueError:
					if self.force_dep:
						print >> sys.stderr, "W: Can not satisfy %s"%(dep_pkg)
						print >> sys.stderr, "W: Building anyways because -f was used"
					else:
						raise DependencyError("E: Couldn't satisfy dependency %s, use -f to override"%dep)
			self.exec_chroot('apt-get install -y %s'%' '.join(apt_list))
		else:
			pass
	def build_package(self):
		ret = os.system('echo "cd %s; dpkg-buildpackage -d -a%s" | chroot %s'%(self.source_dir, self.target_arch, self.chroot_path))
		if ret is not 0:
			# A build error occurred
			print >> sys.stderr, "E: The package failed to build properly, you are entering a root shell"
			print >> sys.stderr, "E: Press ^D to reattempt the build"
			self.exec_chroot('/bin/sh -c "cd %s; /bin/bash"'%(self.source_dir))
			i = raw_input("Reattempt build? [Y/n] ")
			if i=='' or i=='y' or i=='Y':
				self.build_package()
			
	def cleanup(self):
		if self.dest_dir:
			items = os.listdir(self.working_path)
			for i in items:
				if os.path.isfile(self.working_path+'/'+i):
					os.system('mv %s %s'%(self.working_path+'/'+i, self.dest_dir))
		os.system('umount %s/env/%s%s'%(WORKING_DIR, self.distro_name, WORKING_DIR))
		os.system('rm -rf '+WORKING_DIR+'/env/' + self.distro_name)
		os.system('exit')
		print >> sys.stderr, "I: All done"
	def exec_chroot(self, arg):
		os.system('chroot %s %s'%(self.chroot_path, arg))
	def build(self):
		self.check_for_env()
		try:
			self.init_working_path()
			self.prepare_source()
			self.mount_build_env()
			self.dch_version()
			self.check_depends()
			self.build_package()
		except Exception, e:
			print e
		finally:
			self.cleanup()
	

class BackportFromDsc(Backport):
	def __init__(self, dsc):
		self.dsc = dsc
	def prepare_source(self):
		os.chdir(self.working_path)
		if self.dsc.startswith('http://') or self.dsc.startswith("ftp://") or self.dsc.startswith("https://"):
			os.system('dget -x %s'%self.dsc)
		else:
			os.system('dpkg-source -x %s'%self.dsc)
		for i in os.listdir(self.working_path):
			if os.path.isdir(os.path.join(self.working_path, i)):
				os.chdir(os.path.join(self.working_path, i))
				self.source_dir = os.path.join(self.working_path, i)
				break

class BackportFromAPT(Backport):
	def __init__(self, package):
		self.package = package
	def prepare_source(self):
		pass

class BackportFromDir(Backport):
	def __init__(self):
		self.orig_dir = os.getcwd()
	def prepare_source(self):
		os.chdir(self.working_path)
		os.system('cp -R %s .'%self.orig_dir)
		self.source_dir = os.path.join(self.working_path, os.path.basename(self.orig_dir))

if __name__ == "__main__":
	parser = OptionParser(usage="%prog [options] [arguments]", version="0.1.0")
	parser.add_option("--dist", "-d", dest="dist", action="store",
		default=None, help="The distribution to build against")
	parser.add_option("--arch", "-a", dest="arch", action="store",
		default=None, help="The architecture to build for")
	parser.add_option("--put", "-p", dest="put", action="store",
		default=False, help="Where to put the finished packages")
	parser.add_option("--no-suffix", dest="suffix", action="store_false",
		default=True, help="Do not append the ~X.XXzbuild1 suffix to package version")
	parser.add_option("--force-depends", "-f", dest="force", action="store_true",
		default=False, help="Ignore version number of dependencies")
	(options, arguments) = parser.parse_args()
	if len(arguments) > 0:
		cmd = arguments[0]
		args = arguments[1:]
	else:
		print >> sys.stderr, "E: You did not specify an action"
		sys.exit(0)
	if options.dist is None:
		options.dist = get_current_distro()
		print >> sys.stderr, "I: Using currently running distro:", options.dist
	else:
		print >> sys.stderr, "I: Using specified distro:", options.dist
	if options.arch is None:
		options.arch = get_current_arch()
		print >> sys.stderr, "I: Using currently running arch:", options.arch
	else:
		print >> sys.stderr, "I: Using specified arch:", options.arch
	if cmd == "create":
		create_build_env(options.dist, options.arch)
	elif cmd == "build":
		if len(args) == 1 and args[0].endswith(".dsc"):
			b = BackportFromDsc(args[0])
		elif len(args) == 0:
			b = BackportFromDir()
		else:
			pass
		if b:
			b.store_values(options.dist, options.arch, options.put, options.suffix, options.force)
			b.build()
	else:
		print >> sys.stderr, "E: The command, %s, is invalid"%cmd
			

